package com.googlecode.curvedmetromaps.core;

import static playn.core.PlayN.*;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jbox2d.common.Vec2;

import com.googlecode.curvedmetromaps.core.logic.costfunction.CostSystem;
import com.googlecode.curvedmetromaps.core.logic.forcedriven.ForceSystem;
import com.googlecode.curvedmetromaps.core.logic.localsearch.geneticalgorithm.GeneticAlgorithm;
import com.googlecode.curvedmetromaps.core.logic.localsearch.hillclimbing.HillClimbing;
import com.googlecode.curvedmetromaps.core.model.MetroEdge;
import com.googlecode.curvedmetromaps.core.model.MetroMap;
import com.googlecode.curvedmetromaps.core.model.MetroStation;
import com.googlecode.curvedmetromaps.core.model.importer.MapLoader;
import com.googlecode.curvedmetromaps.core.presentation.Renderer;
import com.googlecode.curvedmetromaps.core.presentation.SvgGenerator;

import playn.core.Canvas;
import playn.core.CanvasImage;
import playn.core.Game;
import playn.core.ImageLayer;
import playn.core.Key;
import playn.core.Keyboard;
import playn.core.Mouse;
import playn.core.Mouse.MotionEvent;
import playn.core.Mouse.WheelEvent;
import playn.core.PlayN;
import playn.core.SurfaceImage;
import playn.core.util.Callback;

public class MetroMapr extends Game.Default {

	// Presentation
	private CanvasImage			bgCanvasImage;
	private CanvasImage			mapCanvasImage;
	private SurfaceImage		mapSurface;
	private ImageLayer			mapLayer;
	private Renderer			renderer;
	private SvgGenerator		svgGenerator;

	// Camera
	private float				camZoom		= 1;
	private float				camX		= 0;
	private float				camY		= 0;
	
	// Input
//	private String inputPath = "maps/json/dutchintercities.json";
//	private String inputPath = "maps/osm/Paris.xml";
//	private String inputPath = "maps/osm/Madrid.xml";
//	private String inputPath = "maps/graphml/wien.graphml";
//	private String inputPath = "maps/osm/Amsterdam.xml";
//	private String inputPath = "maps/json/len4.json";
//	private String inputPath = "maps/graphml/sydney.graphml";
//	private String inputPath = "maps/graphml/karlsruhe.graphml";
//	private String inputPath = "maps/graphml/montreal.graphml";
//	private String inputPath = "maps/graphml/washington.graphml";
//	private String inputPath = "maps/graphml/london.graphml";

	// Main metro map
	private MetroMap			metroMap;
	private boolean				isMapLoaded	= false;
	private boolean				hasWrittenSvg = false;

	// Force system
	private ForceSystem			forceSystem;
	private double simulationTime = 0;

	// Search algorithms
	private GeneticAlgorithm    geneticAlgorithm;
	private HillClimbing hillClimbing;

	// Cost system
	private CostSystem			costSystem;
	private String costPath = null;
	private boolean isFirstLine = true;

	public MetroMapr() {
		// Set time in ms between update calls
		super(1);
	}

	@Override
	public void init() {
		int w = graphics().width();
		int h = graphics().height();

		mapSurface = graphics().createSurface(w, h);

		// Create layer that contains the surface
		mapLayer = graphics().createImageLayer();
		mapLayer.setImage(mapSurface);
		graphics().rootLayer().add(mapLayer);

		// create canvas to render map
		bgCanvasImage = graphics().createImage(w, h);
		mapCanvasImage = graphics().createImage(w, h);

		// create metro renderer
		renderer = new Renderer();

		// Load the metro map
		createMetroMap(inputPath);

		// Mouse events
		setMouseListeners();
	}
	
	void bezierProjectionTest() {
		MetroStation m0 = new MetroStation(0f, 0f);
		MetroStation m1 = new MetroStation(1f, 0f);

		for (float x = 1f / 2f; x <= 1f; x += 1f / 6f) {
			Vec2 p = new Vec2(x, 1f);
			MetroEdge e = new MetroEdge(m0, m1);
			Vec2 result = e.pointProjection(p);
			if (result.x == x && result.y == 0f) {
				System.out.print("Correct result: ");
			} else {
				System.out.print("WRONG result: ");
			}
			System.out.println(String.valueOf(result.x) + "," + String.valueOf(result.y));
		}
	}

	@Override
	public void update(int delta) {
		if (isMapLoaded == false)
			return;

		float w = graphics().width();
		float h = graphics().height();
		float p = renderer.padding;
		metroMap.aspectRatio = ((1 - p) * w) / ((1 - p) * h);

		// Instantiate classes for manipulated the MetroMap
		if (forceSystem == null)
			forceSystem = new ForceSystem(metroMap);
		if (costSystem == null)
			costSystem = new CostSystem(metroMap);
		if (geneticAlgorithm == null)
			geneticAlgorithm = new GeneticAlgorithm(metroMap);
		if (hillClimbing == null)
			hillClimbing = new HillClimbing(metroMap);
		
		// Create class to write final output
		if (svgGenerator == null)
			svgGenerator = new SvgGenerator(metroMap);

//		metroLine = geneticAlgorithm.update();
//		metroLine = hillClimbing.update();
		
		// Date output path part
		SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss");
		String datePart = sdf.format(new Date()); 
		
		// Filename output path part
		String patternStr="([^\\/]+)\\.\\w+$";
		Pattern pattern = Pattern.compile(patternStr);
		Matcher matcher = pattern.matcher(inputPath);
		String mapPart = "unnamed";
		if(matcher.find()){
			mapPart = matcher.group(1);
		}

		// Output directory
		File baseDirectory = new File(System.getProperty("user.dir"));
		baseDirectory = new File(baseDirectory, "\\output");
		
		if (costPath == null) {
			String costFilename = mapPart + "_cost_" + datePart + ".txt";
			costPath = (new File(baseDirectory, costFilename)).getAbsolutePath();
			// Write initial costs
			writeCostsToFile(costPath);
		}

		if (forceSystem.cpuTime < simulationTime) {
			// Calculate forces and update map
			forceSystem.update(delta);
			System.out.println(forceSystem.cpuTime + "\t" + forceSystem.maxForceFactor + "\t" + forceSystem.minMaxForceFactor);

			// Write costs
			writeCostsToFile(costPath);
		} else if (hasWrittenSvg == false) {
			// Create SVG path
			String svgFilename = mapPart + "_" + datePart + ".svg";
			String svgPath = (new File(baseDirectory, svgFilename)).getAbsolutePath();
			
			svgGenerator.write(svgPath);
			hasWrittenSvg = true;
			
			System.out.println("SVG output written to: " + svgPath);
		}
	}
	
	private void writeCostsToFile(String path) {
		HashMap<String,Double> costs = costSystem.calculateCosts();
        
		String s = "";
		
		if (isFirstLine) {
			isFirstLine = false;
	        // Write column names
			s += "Time";
			for (String key : costs.keySet()) {
				s += "\t" + key;  
			}
			s += "\n";
		}
		
        // Write values
		s += forceSystem.cpuTime;
		for (String key : costs.keySet()) {
			double cost = costs.get(key);
			s += "\t" + cost;  
		}
		s += "\n";

		BufferedWriter writer = null;
        try {
            //create a temporary file
            File file = new File(path);

            writer = new BufferedWriter(new FileWriter(file, true));
    		writer.write(s);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                // Close the writer regardless of what happens...
                writer.close();
            } catch (Exception e) {
            }
        }
	}

	@Override
	public void paint(float alpha) {
		int w = graphics().width();
		int h = graphics().height();

		Canvas bgCanvas = bgCanvasImage.canvas();
		bgCanvas.setFillColor(0xFFFFFFFF);
		bgCanvas.fillRect(0, 0, w, h);

		Canvas canvas = mapCanvasImage.canvas();
		canvas.clear();
		if (isMapLoaded) {
			renderer.drawMetroMap(canvas, graphics(), metroMap);
		}

		mapSurface.surface().drawImage(bgCanvasImage, 0, 0);
		mapSurface.surface().drawImage(mapCanvasImage, 0, 0);
	}

	private void createMetroMap(String map) {
		// Load metro map
		MapLoader.load(map, new Callback<MetroMap>() {
			@Override
			public void onSuccess(MetroMap resource) {
				metroMap = resource;
				isMapLoaded = true;
			}

			@Override
			public void onFailure(Throwable err) {
				PlayN.log().error("Error loading metro map: " + err.getMessage());
			}
		});
	}

	private void setMouseListeners() {
		keyboard().setListener(new Keyboard.Adapter() {
			@Override
			public void onKeyDown(Keyboard.Event event) {
				float x = 0, y = 0;
				Key key = event.key();
				if (key == Key.LEFT) {
					x = 1;
				} else if (key == Key.RIGHT) {
					x = -1;
				} else if (key == Key.UP) {
					y = 1;
				} else if (key == Key.DOWN) {
					y = -1;
				} else if (key == Key.NP_SUBTRACT || key == Key.PAGE_DOWN) {
					forceSystem.speed /= 1.5f;
				} else if (key == Key.NP_ADD  || key == Key.PAGE_UP) {
					forceSystem.speed *= 1.5f;
				}

				if (x != 0 || y != 0) {
					Canvas canvas = mapCanvasImage.canvas();
					float dx = 0.95f * canvas.width() * x / camZoom;
					float dy = 0.95f * canvas.height() * y / camZoom;
					canvas.translate(dx, dy);
				}
			}
		});
		
		// add a listener for pointer (mouse, touch) input
		mouse().setListener(new Mouse.Adapter() {
			@Override
			public void onMouseWheelScroll(WheelEvent event) {
				// Zoom
				float z = (float) Math.pow(2f, -event.velocity());
				// Translation
				float x = ((event.localX() - camX) * (1 - z));
				float y = ((event.localY() - camY) * (1 - z));

				// Perform translate, scale on canvas.
				Canvas canvas = mapCanvasImage.canvas();
				canvas.translate(x / camZoom, y / camZoom);
				canvas.scale(z, z);

				camZoom *= z;
				camX += x;
				camY += y;
			}

			@Override
			public void onMouseMove(MotionEvent event) {
				renderer.mouseX = event.localX();
				renderer.mouseY = event.localY();
			}
		});
	}
}
